﻿using Sustainalytics.ClientDW.DataAccessLayer;
using Sustainalytics.ClientDW.Entities;
using Sustainalytics.ClientDW.Entities.Alerts;
using Sustainalytics.DataLayer.ConnectionStrings;
using Sustainalytics.Entities;
using Sustainalytics.Entities.ConnectionStrings;
using Sustainalytics.Utils.Configuration;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Net.Mail;
using System.Threading;
using System.Threading.Tasks;

namespace Sustainalytics.Alerts.AlertsEngine
{
    public class AlertsEngine : IAlertsEngine
    {
        private const int _maxClientAlertsSendingAttempts = 5;
        private readonly long _alertsTimerStartDelay = 5 * 60 * 1000;
        private readonly int _alertsTimerPollingInterval = 10 * 60 * 1000;// 100'
       
        private static object _syncObject = new object();

        private string _SustSupportEmail;

        private static AlertsEngine _alertsEngine;
        private bool _processingAlerts = false;
        private readonly DeploymentLocation _deploymentLocation;
        private int _clientAlertsSendingAttempts = 0;
        private readonly IAlertsSubscription _alertsSubscription;
        private readonly IAlertsManager _alertsManager;
        private readonly IConnectionStringsManager _connectionStringManager;
        private readonly IPorfolioNamesCache _portfolioCacheName;
        private readonly IPortfolioCompanyCache _portfolioCompanyCache;
        private readonly IFeatureUniversesCache _featureUniversesCache;
        private Guid _sessionId;
        private Timer _alertsTimer;
        private readonly string _connectionStringClientPlatform;
        private readonly IBlobHelper _blobHelper;
        private readonly string _senderEmail;
        private readonly long _delayBetweenEmails = 5000;
        private readonly long _lastSyncMaxDelay = 24 * 3600;
        private readonly string _mongoDbConnString;

        private AlertsEngine(){}
        public AlertsEngine(
            DeploymentLocation deploymentLocation, 
            IBlobHelper blobHelper,
            IAlertsSubscription alertsSubscription, 
            IAlertsManager alertsManager, 
            IConnectionStringsManager connectionStringManager, 
            string connectionStringClientPlatform, 
            IPorfolioNamesCache portfolioCacheName, 
            IPortfolioCompanyCache portfolioCompanyCache, 
            IFeatureUniversesCache featureUniversesCache)
        {
            _deploymentLocation = deploymentLocation;
            _alertsManager = alertsManager;
            _blobHelper = blobHelper;
            _alertsSubscription = alertsSubscription;
            _connectionStringManager = connectionStringManager;
            _connectionStringClientPlatform = connectionStringClientPlatform;
            _portfolioCacheName = portfolioCacheName;
            _portfolioCompanyCache = portfolioCompanyCache;
            _featureUniversesCache = featureUniversesCache;


            if (long.TryParse(ConfigurationManager.AppSettings["AlertsTimerStartDelay"], out long alertsTimerStartDelay))
            {
                _alertsTimerStartDelay = alertsTimerStartDelay;
            }
            if (int.TryParse(ConfigurationManager.AppSettings["AlertsTimerPollingInterval"], out int alertsTimerPollingInterval))
            {
                _alertsTimerPollingInterval = alertsTimerPollingInterval;
            }
            if (long.TryParse(ConfigurationManager.AppSettings["DelayBetweenEmails"], out long delayBetweenEmails))
            {
                _delayBetweenEmails = delayBetweenEmails;
            }
            if (long.TryParse(ConfigurationManager.AppSettings["LastSyncMaxDelay"], out long lastSyncMaxDelay))
            {
                _lastSyncMaxDelay = lastSyncMaxDelay;
            }

            var connString = ConfigurationManager.ConnectionStrings["MongoDBConnectionString"];
            if (connString == null)
                throw new Exception("MongoDBConnectionString key is missing!");

            _mongoDbConnString = connString.ConnectionString;
            _senderEmail = ConfigurationManager.AppSettings["SenderEmail"];
        }


        public static IAlertsEngine GetAlertsEngine(DeploymentLocation deploymentLocation,
            IBlobHelper blobHelper, IAlertsSubscription alertsSubscription, IAlertsManager alertsManager, IConnectionStringsManager connectionStringManager, 
            string connectionStringClientPlatform, IPorfolioNamesCache portfolioCacheName, IPortfolioCompanyCache portfolioCompanyCache, IFeatureUniversesCache featureUniversesCache)
        {
            lock (_syncObject)
            {
                if (_alertsEngine == null)
                {
                    _alertsEngine = new AlertsEngine(deploymentLocation, blobHelper, alertsSubscription, alertsManager, 
                        connectionStringManager, connectionStringClientPlatform, portfolioCacheName, portfolioCompanyCache, featureUniversesCache);
                }

                return _alertsEngine;
            }
        }

        public void StartAlertsTimer()
        {
            if (_alertsTimer == null)
            {
                _SustSupportEmail = ConfigurationManager.AppSettings["SustSupportEmail"];
                //start the timer with a due time of 5' so that a debugger could eventually be attached
                _alertsTimer = new Timer(ProcessAlerts, null, _alertsTimerStartDelay, _alertsTimerPollingInterval);
            }
        }

        private DateTime GetSemiMonthlyBasedDate(DateTime date)
        {
            return new DateTime(date.Year, date.Month, 1 + 15 * (date.Day / 16));
        }

        private DateTime GetNextSemimonthlyBasedDate(DateTime date)
        {
            return GetSemiMonthlyBasedDate(GetSemiMonthlyBasedDate(date).AddDays(16));
        }

        private DateTime GetLastDayOfNextSemimonthlyBasedDate(DateTime date)
        {
            return GetNextSemimonthlyBasedDate(GetNextSemimonthlyBasedDate(date)).AddDays(-1);
        }

        private void ProcessAlerts(object timerState)
        {
            if (!_processingAlerts)
            {
                _sessionId = Guid.NewGuid();
                try
                {
                    _processingAlerts = true;

                    AlertsEngineSerilog.Log.ProcessAlertStarted(_sessionId);

                    _blobHelper.ReadBlob();
                    if (_blobHelper.UnservedClients.Count > 0)
                    {
                        AlertsEngineSerilog.Log.FileRead(_sessionId, _blobHelper.NextAlertSessionSendingDate, _blobHelper.LastSemiMonthlyAlertSession,
                            _blobHelper.UnservedClients.Select(g => g.ToString()).Aggregate((i, j) => i + " " + j));
                    }
                    else
                    {
                        AlertsEngineSerilog.Log.FileRead(_sessionId, _blobHelper.NextAlertSessionSendingDate, _blobHelper.LastSemiMonthlyAlertSession,
                            string.Empty);
                    }


                    if (DateTime.UtcNow.Ticks > _blobHelper.NextAlertSessionSendingDate.Ticks &&
                        (_clientAlertsSendingAttempts > 0 || WasSuccesfullyLastSync()) &&
                        _clientAlertsSendingAttempts < _maxClientAlertsSendingAttempts)
                    {
                        var connectionStrings = _connectionStringManager.GetConnectionStrings(new List<ConnectionStringType>() { ConnectionStringType.ClientDW, ConnectionStringType.ClientUniverses });

                        var watch = Stopwatch.StartNew();
                        FillAlerts(connectionStrings, _blobHelper.UnservedClients, _sessionId, out Task subsTask);

                        SendAlerts(subsTask);
                        watch.Stop();

                        Console.WriteLine(String.Format("Send alerts took: {0}s", (double)watch.ElapsedMilliseconds / 1000));
                    }
                    else
                    {
                        AlertsEngineSerilog.Log.NoNeedToProcess(_sessionId);
                    }
                    AlertsEngineSerilog.Log.ProcessAlertEnded(_sessionId);
                }
                catch (Exception e)
                {
                    var issue = "General error encountered while processing current alerts session";
                    AlertsEngineSerilog.Log.Exception(_sessionId, issue, e);
                    NotifySupportChannel(issue, "Exception details: <br/>" + e);
                }
                finally
                {
                    try
                    {
                        _alertsManager.CleanUp();
                    }
                    catch (Exception ex)
                    {
                        var issue = "Internal error encountered while processing current alerts session.";
                        AlertsEngineSerilog.Log.Exception(_sessionId, issue, ex);
                        NotifySupportChannel(issue, "Exception details: <br/>" + ex);
                    }

                    _processingAlerts = false;
                }
            }
        }

        private bool WasSuccesfullyLastSync()
        {
            var clientDwMasterRepository = ClientDWMasterRepository.GetInstance(_mongoDbConnString);
            var lastSyncLog = clientDwMasterRepository.GetLastStartedSync();
            if (lastSyncLog == null)
            {
                AlertsEngineSerilog.Log.NoLastSync(_sessionId);
                return false;
            }

            if (lastSyncLog.TimeEnd == null)
            {
                AlertsEngineSerilog.Log.LastSyncNotEnded(_sessionId);
                return false;
            }

            if (DateTime.UtcNow.AddSeconds(-_lastSyncMaxDelay).Ticks > lastSyncLog.TimeEnd.Value.Ticks)
            {
                AlertsEngineSerilog.Log.LastSyncOlderThan24Hours(_sessionId);
                return false;
            }

            return lastSyncLog.Status == LastSessionStatus.Finished;
        }

        private void SendAlerts(Task subsTask)
        {
            Stopwatch watch;
            Tuple<MailMessage, List<Guid>, Guid> a;

            while (true)
            {
                if (!_alerts.TryDequeue(out a))
                {
                    if (subsTask.IsCanceled || subsTask.IsCompleted || subsTask.IsFaulted)
                    {
                        subsTask.Wait();
                        break;
                    }
                    Thread.Sleep(100);
                }
                else
                {
                    try
                    {
                        if (a.Item1 == null)//no need to send any email
                        {
                            _blobHelper.UnservedClients.Remove(a.Item3);
                            WriteBlob();
                            continue;
                        }

                        SendEmail(a.Item1);

                        watch = Stopwatch.StartNew();

                        _blobHelper.UnservedClients.Remove(a.Item3);

                        WriteBlob();
                        AlertsEngineSerilog.Log.EmailSent(_sessionId, "User: " + a.Item3.ToString(), a.Item1.To[0].Address);

                        watch.Stop();

                        var delay = _delayBetweenEmails - watch.ElapsedMilliseconds;
                        if (delay > 0)
                        {
                            Thread.Sleep(TimeSpan.FromMilliseconds(delay));// let the email service do its devious job
                        }
                    }
                    catch (Exception e)
                    {
                        var issue = string.Format("{0} - Error while sending email for user {1} to email address {2} {3}{4}", DateTime.Now, a.Item3, a.Item1.To[0], Environment.NewLine, e);

                        AlertsEngineSerilog.Log.Exception(_sessionId, issue, e);
                    }
                }
            }

            if (_blobHelper.UnservedClients.Count == 0)
            {
                _blobHelper.LastSemiMonthlyAlertSession = _blobHelper.NextAlertSessionCollectionDate;
                //_blobHelper.NextAlertSessionCollectionDate = DO NOT increment it automatically anymore to avoid the risk of sending out unreviewed alerts
                var nextAlertSessionSendingDate = GetLastDayOfNextSemimonthlyBasedDate(_blobHelper.NextAlertSessionSendingDate);
                _blobHelper.NextAlertSessionSendingDate = nextAlertSessionSendingDate
                    .AddHours(_blobHelper.NextAlertSessionSendingDate.Hour)
                    .AddMinutes(_blobHelper.NextAlertSessionSendingDate.Minute);

                WriteBlob();

                _clientAlertsSendingAttempts = 0;

                NotifySupportChannel(string.Format("Client alerts session {0:yyyy-MM-dd HH:mm}", _blobHelper.LastSemiMonthlyAlertSession), "Successfully completed.");
                AlertsEngineSerilog.Log.CompletedSuccesfully(_sessionId);
            }
            else if (++_clientAlertsSendingAttempts == _maxClientAlertsSendingAttempts)
            {
                NotifySupportChannel(
                    string.Format("Client alerts session {0:yyyy-MM-dd HH:mm}", _blobHelper.NextAlertSessionSendingDate),
                    "Failed to send alert to the following users: <br/><br/>" + string.Join("<br/>",
                    _blobHelper.UnservedClients));
            }
        }

        private class PeriodInfo
        {
            public int Frequency { get; set; }
            public DateTime StartDate { get; set; }
            public DateTime EndDate { get; set; }
            public List<Alert> Alerts { get; set; }
        }

        private void FillAlerts(IDictionary<ConnectionStringType, string> connectionStrings, List<Guid> unservedClients, Guid sessionId, out Task subsTask)
        {
            List<PeriodInfo> periodsInfo = new List<PeriodInfo>();
            FillPeriods(periodsInfo);
            RetrieveAlerts(periodsInfo, connectionStrings, sessionId);
            var subs = new List<SubscribeToPortfolio>();
            var dictSubPeriod = new Dictionary<SubscribeToPortfolio, PeriodInfo>();
            RetrieveSubscriptions(periodsInfo, unservedClients, subs, dictSubPeriod);

            _blobHelper.UnservedClients = subs.Select(s => s.UserId).ToList();
            subsTask = Task.Run(() =>
            {
                var emailBuilder = new EmailBuilder();
                Parallel.ForEach(subs,
                    new ParallelOptions() { MaxDegreeOfParallelism = SettingsHelper.Read("MaxDegreeOfParallelism", 1) },
                    sub =>
                    {
                        var period = dictSubPeriod[sub];
                        ProcessSubscription(sub, connectionStrings, sessionId, period.Alerts, period.StartDate,
                            period.EndDate, emailBuilder, _portfolioCacheName, _portfolioCompanyCache,
                            _featureUniversesCache);
                    });
            });
        }

        private void FillPeriods(List<PeriodInfo> periodsInfo)
        {
            periodsInfo.Add(new PeriodInfo() {
                Frequency = AlertsManager.Semimonthly,
                StartDate = _blobHelper.LastSemiMonthlyAlertSession,
                EndDate = _blobHelper.NextAlertSessionCollectionDate
            });
        }

        private void RetrieveAlerts(List<PeriodInfo> periodsInfo, IDictionary<ConnectionStringType, string> connectionStrings, Guid sessionId)
        {
            foreach (var period in periodsInfo)
            {
                period.Alerts = _alertsManager.GetAlertsFromPeriod(period.StartDate, period.EndDate, connectionStrings[ConnectionStringType.ClientDW], sessionId)
                                            .OrderByDescending(a => a.LevelOfImportance)
                                            .ThenBy(a => a.CompanyAlert.Name).ToList();
            }
        }

        private ConcurrentQueue<Tuple<MailMessage, List<Guid>, Guid>> _alerts = new ConcurrentQueue<Tuple<MailMessage, List<Guid>, Guid>>();

        private void RetrieveSubscriptions(List<PeriodInfo> periods, List<Guid> unservedClients, List<SubscribeToPortfolio> subs, Dictionary<SubscribeToPortfolio, PeriodInfo> dictSubPeriod)
        {
            foreach (var period in periods)
            {
                var localSubs = _alertsSubscription.GetSubscriptions(period.Frequency, unservedClients, _connectionStringClientPlatform);
                subs.AddRange(localSubs);
                foreach (var sub in localSubs)
                    dictSubPeriod.Add(sub, period);
            }
        }

        private void ProcessSubscription(
            SubscribeToPortfolio sub, 
            IDictionary<ConnectionStringType, string> connectionStrings, 
            Guid sessionId,
            List<Alert> alertsForPeriod, 
            DateTime startDate, 
            DateTime endDate, 
            EmailBuilder emailBuilder,
            IPorfolioNamesCache portfolioCacheName, 
            IPortfolioCompanyCache portfolioCompanyCache, 
            IFeatureUniversesCache featureUniversesCache)
        {
            var subDto = _alertsSubscription.GetSubscriptionDetails(sub, sub.Frequency, sessionId,
                connectionStrings[ConnectionStringType.ClientUniverses], _connectionStringClientPlatform, connectionStrings[ConnectionStringType.ClientDW],
                alertsForPeriod.Select(a => a.CompanyId).ToList(),
                portfolioCacheName, portfolioCompanyCache, featureUniversesCache);

            if (subDto.Enabled)
            {
                string message = "two weeks";
                _alerts.Enqueue(emailBuilder.GetMessageForSubscription(startDate, endDate, message, subDto,
                                        connectionStrings[ConnectionStringType.ClientDW],
                                        alertsForPeriod.Where(a => subDto.CompaniesWithPortfolios.ContainsKey(a.CompanyId)).ToList()));
            }
            else
            {
                _alerts.Enqueue(new Tuple<MailMessage, List<Guid>, Guid>(null, null, sub.UserId));
            }
        }

        private void WriteBlob()
        {
            try
            {
                _blobHelper.WriteBlob();
            }
            catch (TimeoutException ex)
            {
                NotifySupportChannel("Failed to upload alert session info to blob", ex.Message);
            }
        }

        private void NotifySupportChannel(string subject, string message)
        {
            try
            {
                using (MailMessage mailMessage = new MailMessage()
                {
                    Subject = subject + " - " + (_deploymentLocation == DeploymentLocation.Development ? Environment.MachineName.ToUpperInvariant() : _deploymentLocation.ToString()),
                    Body = message,
                    IsBodyHtml = true,
                    From = new MailAddress(_senderEmail)
                })
                {
                    mailMessage.To.Add(new MailAddress(_SustSupportEmail));
                    SendEmail(mailMessage);
                }
            }
            catch (Exception e)
            {
                var issue = string.Format("{0} - Failed to send notifications onto the support channel! {1}{2}", DateTime.Now, Environment.NewLine, e);
                AlertsEngineSerilog.Log.Exception(_sessionId, issue, e);
            }
        }

        private void SendEmail(MailMessage message)
        {
            SmtpClient mailClient = new SmtpClient();
            try
            {
                mailClient.Send(message);
            }
            catch (Exception ex)
            {
                AlertsEngineSerilog.Log.Exception(_sessionId, "Send email error: ", ex);
            }
            finally
            {
                mailClient.Dispose();
            }
        }
    }
}
